#include "Wt.h"

void WtInitExecState(WtExecutionState *execState, WtContext *w)
{
  memset(execState, 0, sizeof(WtExecutionState));
  execState->context = w;
  execState->interp = w->active->server.interp;
}

void WtDeleteExecState(WtExecuteionState *execState)
{
  if (execState->requestedPath) {
    Tcl_DecrRefCount(execState->requesedPath);
    execState->requestedPath = NULL;
  }

  if (execState->targetPath) {
    Tcl_DecrRefCount(execState->targetPath);
    execState->targetPath = NULL;
  }

  if (execState->genCode) {
    Tcl_DecrRefCount(execState->genCode);
    execState->genCode = NULL;
  }
}

int WtFindAndExecuteFile(WtContext *w, Tcl_Obj *requestedPath)
{
  int ok = 0;
  WtExecutionState execState;

  WtInitExecState(&execState, w);

  execState.requestedPath = requestedPath;
  Tcl_IncrRefCount(execState.requestedPath);

  execState.targetPath = WtFindFile(&execState);

  if (execState.targetPath) {
    Tcl_IncrRefCount(execState.targetPath);
    ok = WtExecuteFile(&execState);
  }

  WtDeleteExecState(&execState)

  return ok;
}

Tcl_Obj *WtFindFile(WtExecutionState *execState)
{
  Tcl_Obj *path = WtResolvePath(execState->context, execState->requestedPath, NULL);
  if (!path) {
    Tcl_AppendResult(execState->interp, "Failed to read file \"",
                     path, "\": ", Tcl_PosixError(execState->interp),
                     ".", NULL);
  }
  return path;
}

Tcl_Obj *WtResolvePath(WtContext *w, Tcl_Obj *requestedPath, Tcl_Obj *dirs)
{
  int i, len;
  char *bytes, *parts[2], *p;
  Tcl_PathType pathType;
  Tcl_DString joined;
  Tcl_Obj *resolved = NULL;

  bytes = Tcl_GetStringFromObj(requestedPath, &len);
  pathType = Tcl_GetPathType(bytes);

  if (pathType == TCL_PATH_ABSOLUTE || pathType == TCL_PATH_VOLUME_RELATIVE) {
    return Tcl_NewStringObj(bytes, len);
  }

  if (!dirs) {
    dirs = WtTableGetObjFromStr(w->generalSet, "includeSearchPath", NULL);
  }

  if (!dirs) {
    return NULL;
  }

  for (i = 0; i < WtGetListSize(dirs); ++i) {
    parts[0] = Tcl_GetStringFromObj(WtGetListItem(dirs, i), NULL);
    parts[1] = bytes;

    Tcl_DStringInit(&joined);
    p = Tcl_JoinPath(2, parts, &joined);

    if (p && Tcl_Access(p, 00) == 0) {
      resolved = Tcl_NewStringObj(p, -1);
    }

    Tcl_DStringFree(&joined);

    if (resolved) {
      return resolved;
    }
  }

  return NULL;
}

int WtExecuteFile(WtExecutionState *execState)
{
  int ok = 0, n;
  Tcl_Channel chan = NULL;
  Tcl_Obj *text = NULL;

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
    "Wtcl: WtExecuteFile (targetPath = \"%s\").",
    Tcl_GetString(execState->targetPath));

  chan = Tcl_OpenFileChannel(execState->interp,
    Tcl_GetString(execState->targetPath),
    "r", 0666);

  if (chan) {
    if (Tcl_SetChannelOption(execState->interp, chan, "-translation",
        "binary") == TCL_OK) {
      text = Tcl_NewObj();
      Tcl_IncrRefCount(text);
      n = Tcl_ReadChars(chan, text, -1, 0);
      if (n == -1) {
        Tcl_AppendResult(execState->interp, "Failed to read file \"",
          execState->targetPath, "\":", Tcl_PosixError(execState->interp), ".", NULL);
      } else {
        ok = WtExecuteString(execState, text, n);
      }
    }
    if (Tcl_Close(execState->interp, chan) != TCL_OK) {
      ok = 0;
    }
  }

  return ok;
}

int WtExecuteString(WtExecutionState *execState, Tcl_Obj *string, int len)
{
  int ok = 1, rc, savedResult;
  Tcl_Obj *startScript, *endScript, *frame;
  Tcl_SavedResult interpResult;

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
    "Wtcl: WtExecuteString (len = %d).", len);

  /* Add script to stack */

  frame = WtNewTableObj();
  Tcl_IncrRefCount(frame);

  WtTableSetStrToObj(frame, "path", execState->targetPath ? execState->targetPath :
                     WtNewString(NULL));
  WtTableSetStrToStr(frame, "state", "start");
  WtTableSetStrToStr(frame, "code", "");
  WtTableSetStrToBool(frame, "fault", 0);

  WtAppendToList(execState->context->scriptStack, frame);

  /* Start hook */

  startScript = execState->context->active->server.includeStartScript;
  if (Tcl_GetCharLength(startScript)) {
    if (Tcl_Eval(execState.interp, startScript) == TCL_ERROR) {
      ok = 0;
      WtTableSetStrToBool(frame, "fault", 1);
    }
  }

  /* Parse script */

  if (ok) {
    WtTableSetStrToStr(frame, "state", "parse");
    ok = WtParseString(execState, string, len);
    if (!ok) {
      WtTableSetStrToBool(frame, "fault", 1);
    }
    WtTableSetStrToObj(frame, "code", execState->genCode ? execState->genCode :
      WtNewString(NULL));
  }

  /* Run the script */

  if (ok) {

    WtTableSetStrToStr(frame, "state", "run");

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
      "Wtcl: WtExecuteString: Calling Tcl_EvalObjEx.");

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
      "Wtcl: WtExecuteString: interp = 0x%x, genCode = \"%s\".",
      execState->interp, Tcl_GetString(execState->genCode));

    rc = Tcl_EvalObjEx(execState->interp, execState->genCode, 0);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
      "Wtcl: WtExecuteString: Tcl_EvalObjEx returned %d.", rc);

    if (rc != TCL_OK) {
      ok = 0;
      WtTableSetStrToBool(frame, "fault", 1);
    }
  }

  /* End hook */

  WtTableSetStrToStr(frame, "state", "end");

  endScript = execState->context->active->server.includeEndScript;

  if (endScript) {
    if (ok) {
      Tcl_SaveResult(interp, &interpResult);
      savedResult = 1;
    }
    if (Tcl_Eval(execState->interp, endScript) == TCL_ERROR) {
      ok = 0;
      WtTableSetStrToBool(frame, "fault", 1);
    }
    if (savedResult) {
      if (ok) {
        Tcl_RestoreResult(interp, &interpResult);
      } else {
        Tcl_DiscardResult(&interpResult);
      }
    }
  }

  WtTableSetStrToStr(frame, "state", "done");

  /* Pop script stack */

  WtReplaceInList(execState->context->scriptStack,
                  WtGetListSize(execState->context->scriptStack) - 1, 1, 0, NULL);

  Tcl_DecrRefCount(frame);

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
    "Wtcl: WtExecuteString: Returning %d.", ok);

  return ok;
}

/* TODO: Add support for UTF. */

int WtParseString(WtExecutionState *execState, Tcl_Obj *string, int len)
{
  int ok = 1, inExpr = 0;
  char *startStr, *endStr;
  Tcl_Obj *header, *trailer;
  int startLen, endLen;
  char *buf = Tcl_GetString(string);
  char *p = buf, *q, *end = buf + len;
  char exprChar = '=';

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
    "Wtcl: WtParseString (len = %d).", len);

  if (!execState->genCode) {
    execState->genCode = Tcl_NewStringObj(NULL, 0);
    Tcl_IncrRefCount(execState->genCode);
  }

  if (execState->context->active->server.addHeaderTrailer) {
    if (Tcl_GetCharLength(execState->context->active->server.includeHeader)) {
      Tcl_AppendObjToObj(execState->genCode,
        execState->context->active->server.includeHeader);
    }
  }

  if (Tcl_GetCharLength(execState->context->active->server.includeNamespace)) {
    Tcl_AppendToObj(execState->genCode, "namespace eval ", -1);
    Tcl_AppendObjToObj(execState->genCode,
      execState->context->active->server.includeNamespace, -1);
    Tcl_AppendToObj(execState->genCode, " {", 2);
  }

  startStr = execState->context->server.scriptStartString;
  startLen = strlen(startStr);

  endStr = execState->context->server.scriptEndString;
  endLen = strlen(endStr);

  do {
    q = strstr(p, startStr);

    if (q) {
      WtParserAddContent(execState, p, q - p);

      if (q < end - 1 && q[1] == exprChar) {
        inExpr = 1;
        p = q + startLen + 1;
      } else {
        inExpr = 0;
        p = q + startLen;
      }

      q = strstr(p, endStr);

      if (q) {
        if (inExpr) {
          WtParserAddExpr(execState, p, q - p);
        } else {
          WtParserAddCode(execState, p, q - p);
        }
        p = q + endLen;
      } else {
	      if (execState->targetPath) {
          Tcl_AppendResult(execState->interp, "Missing script end tag at end of file \"",
            execState->targetPath, "\".", NULL);
        } else {
          Tcl_AppendResult(execState->interp, "Missing script end tag.",
            NULL);
        }
        ok = 0;
      }
    } else {
      WtParserAddContent(execState, p, end - p);
      break;
    }
  } while (p < end);

  if (ok) {
    if (Tcl_GetCharLength(execState->context->active->server.includeNamespace)) {
      Tcl_AppendToObj(execState->genCode, "\n}", 2);
    }

    if (execState->context->active->server.addHeaderTrailer) {
      if (Tcl_GetCharLength(execState->context->active->server.includeTrailer)) {
        Tcl_AppendObjToObj(execState->genCode,
          execState->context->active->server.includeTrailer);
      }
    }
  }

  return ok;
}

void WtParserAddCode(WtExecutionState *execState, char *buf, int len)
{
  Tcl_AppendToObj(execState->genCode, buf, len);
  Tcl_AppendToObj(execState->genCode, "\n", 1);
}

void WtParserAddExpr(WtExecutionState *execState, char *buf, int len)
{
  Tcl_AppendToObj(execState->genCode, "wt::processScriptExpr ", -1);
  Tcl_AppendToObj(execState->genCode, buf, len);
  Tcl_AppendToObj(execState->genCode, "\n", 1);
}

void WtParserAddContent(WtExecutionState *execState, char *buf, int len)
{
  char *stringLiteral;
  int flags, literalLen;

  literalLen = Tcl_ScanCountedElement(buf, len, &flags);
  stringLiteral = Tcl_Alloc(literalLen);
  ap_assert(stringLiteral);

  literalLen = Tcl_ConvertCountedElement(buf, len, stringLiteral, flags);

  Tcl_AppendToObj(execState->genCode, "wt::processScriptContent ", -1);
  Tcl_AppendToObj(execState->genCode, stringLiteral, literalLen);
  Tcl_AppendToObj(execState->genCode, "\n", 1);

  Tcl_Free(stringLiteral);
}

void WtExecuteRequest(WtContext *w, const char *requestedPath, int initInterp)
{
  int ok = 0;
  WtExecutionState execState;
  Tcl_Obj *startScript, *endScript;

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, w,
    "Wtcl: WtExecuteRequest (requestedPath = \"%s\").",
    requestedPath);

  WtInitExecState(&execState, w);

  execState.requestedPath = Tcl_NewStringObj(requestedPath, -1);
  Tcl_IncrRefCount(execState.requestedPath);

  execState.targetPath = Tcl_NewStringObj(requestedPath, -1);
  Tcl_IncrRefCount(execState.targetPath);

  execState.genCode = NULL;

  // Execute the script

  if (ok) {
    ok = WtExecuteFile(&execState);
    if (!ok) {
      execState.context->active->server.interpErrorState = 1;
      execSTate.context->active->response.status = HTTP_INTERNAL_SERVER_ERROR;
    }
  }

  // Cleanup

  WtDeleteExecState(&execState);

  WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState.context,
    "Wtcl: WtExecuteRequest: Returning %d.", ok);
}

int WtHandleExecutionError(WtExecutionState *execState)
{
  int rc, obj = 1;
  Tcl_Obj *errInfo, *errCode, *objv[1], *msg;
  char *argv[1], *cmdStr;
  WtInterpContext *active = execState->context->active;

  if (execState->targetPath) {
    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, execState->context,
      "Wtcl: Failed to execute file \"%s\": %s",
      WtToString(execState->targetPath),
      Tcl_GetStringResult(execState->interp));
  } else {
    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, execState->context,
      "Wtcl: Failed to execute: %s",
      Tcl_GetStringResult(execState->interp));
  }

  errInfo = Tcl_GetVar2Ex(execState->interp,
                          "errorInfo", NULL, TCL_GLOBAL_ONLY);
  Tcl_IncrRefCount(errInfo);

  errCode = Tcl_GetVar2Ex(execState->interp,
                          "errorCode", NULL, TCL_GLOBAL_ONLY);
  Tcl_IncrRefCount(errCode);

  if (active->server.lastIncludeError) {
    Tcl_DecrRefCount(active->server.lastIncludeError);
  }
  active->server.lastIncludeError = Tcl_GetObjResult(execState->interp);
  Tcl_IncrRefCount(active->server.lastIncludeError);

  if (Tcl_GetCharLength(active->server.errorHandler)) {
    objv[0] = active->server.errorHandler;
    Tcl_IncrRefCount(objv[0]);
    rc = Tcl_EvalObjv(execState->interp, objc, objv, TCL_EVAL_DIRECT);
  } else {
    objv[0] = Tcl_NewStringObj("wt::handleError", -1);
    Tcl_IncrRefCount(objv[0]);
    rc = Tcl_EvalObjv(execState->interp, objc, objv, TCL_EVAL_DIRECT);
    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context,
      "Wtcl: Command {%s} returned %d.", Tcl_GetString(objv[0]), rc);
  }

  if (rc == TCL_ERROR) {

    if (execState->targetPath) {
      WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, execState->context,
        "Wtcl: Error handler failed while executing file \"%s\": %s",
        Tcl_GetString(execState->targetPath),
        Tcl_GetStringResult(execState->interp));
    } else {
      WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, execState->context,
        "Wtcl: Error handler failed: %s",
        Tcl_GetStringResult(execState->interp));
    }

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "a");

    msg = Tcl_NewStringObj("\n    while invoking Wtcl error handler\n\"", -1);
    Tcl_IncrRefCount(msg);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "b");

    argv[0] = Tcl_GetString(objv[0]);
    cmdStr = Tcl_Merge(1, argv);

    if (cmdStr) {
      Tcl_AppendToObj(msg, cmdStr, -1);
    }

    Tcl_Free(cmdStr);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "c");

    Tcl_AppendToObj(msg, "\"", 1);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "d");

    if (errInfo && errCode) {
      Tcl_AppendToObj(msg, "\n    with root error\n{", -1);
      Tcl_AppendObjToObj(msg, errInfo);
      Tcl_AppendToObj(msg, "}", 1);
      Tcl_AppendToObj(msg, "\n    with error code\n\"", -1);
      Tcl_AppendObjToObj(msg, errCode);
      Tcl_AppendToObj(msg, "\"", 1);
      WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "e");
    }

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "f");

    if (execState->targetPath) {
      Tcl_AppendToObj(msg, "\n    while executing Wtcl file\n\"", -1);
      Tcl_AppendObjToObj(msg, execState->targetPath);
      Tcl_AppendToObj(msg, "\"", 1);

      WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "g");
    }

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "h");

    Tcl_AppendResult(execState->interp, Tcl_GetString(msg), NULL);
    Tcl_DecrRefCount(msg);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "i");

    WtPrintTclError(execState->context, execState->interp);

    WtApLog(APLOG_MARK, APLOG_NOERRNO|APLOG_DEBUG, execState->context, "j");

    active->response.status = HTTP_INTERNAL_SERVER_ERROR;
  }

  Tcl_DecrRefCount(objv[0]);

  Tcl_DecrRefCount(errInfo);
  Tcl_DecrRefCount(errCode);
}
